package com.hup.utils.selenium.service;

import com.hup.utils.commons.AssertApp;
import com.hup.utils.commons.TimerPicker;
import com.hup.utils.commons.exception.AppCommonException;
import com.hup.utils.commons.exception.AppExceptionHandler;
import com.hup.utils.commons.exception.ErrInfo;
import com.hup.utils.commons.interfaces.BaseConstant;
import com.hup.utils.selenium.constant.BrowserConstant.BrowserLifeState;
import com.hup.utils.selenium.constant.BrowserConstant.BrowserType;
import com.hup.utils.selenium.exception.WebDriverErrParser;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.log4j.Log4j2;
import org.openqa.selenium.*;
import org.openqa.selenium.interactions.Actions;

import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 浏览器服务类,仅启动(最多)一个浏览器,默认为有界面的({@link #headless});
 * <pre>
 *     多线程问题:本类的浏览器生命周期方法,都是限制单线程访问的,避免在[启动,设置,关闭]浏览器时,多线程相互影响
 *     {@link #setHeadless(boolean)}
 *     {@link #getBrowser()}
 *     {@link #closeBrowser()}
 *     {@link #closeBrowserIfNotTab(WebDriver)}
 * </pre>
 *
 * @author hugan
 * @date 2020/3/20
 */
@Log4j2
public class BrowserService {

    static {
        AppExceptionHandler.registerParser(new WebDriverErrParser());
    }

    private final BrowserType browserType;
    private final TimerPicker timerPicker;
    /**
     * 用于控制单线程访问[浏览器]的生命周期
     */
    private final ReentrantLock lock;
    private BrowserLifeState browserLifeState = BrowserLifeState.ACCESSIBLE;

    /**
     * 浏览器状态回调监听器;
     * 浏览器被非程序关闭时,状态可能会不正确 todo 使用轮询检查?
     */
    @Setter
    private BrowserListener browserListener;
    /**
     * 是否无窗口:默认是有窗口的
     */
    @Getter
    private boolean headless = false;

    private WebDriver browser;

    /**
     * @param browserType {@link BrowserType#desc}
     * @param driveLoc 浏览器driver的路径
     */
    public BrowserService(String browserType, String driveLoc) {
        //检查和设置WebDriver的driver环境变量
        AssertApp.exists(driveLoc, "浏览器驱动");
        this.browserType = BaseConstant.getValue(BrowserType.class, browserType);
        System.setProperty(this.browserType.driverKey, driveLoc);
        timerPicker = new TimerPicker();
        lock = new ReentrantLock(true);
    }

    /**
     * 构造一个已启动且马上可用的浏览器
     *
     * @apiNote 如果浏览器首次启动或被重新打开, 则这是个耗时的过程
     */
    private void testOpenBrowser() {
        try {
            timerPicker.markStart0();
            if (browser != null) {//可能复用 已打开的浏览器
                Set<String> windowHandles = null;
                try {
                    /*
                    处理页签句柄或浏览器关闭的情况
                    1.浏览器关闭:getWindowHandles异常
                    2.旧页签句柄丢失:getWindowHandles正常,getWindowHandle异常
                     */
                    windowHandles = browser.getWindowHandles();
                    log.debug("windowHandles={}", windowHandles);

                    String windowHandle = browser.getWindowHandle();
                    log.debug("windowHandle={}", windowHandle);
                    //都正常:复用已有浏览器
                    return;
                } catch (NoSuchWindowException e) {
                    AppExceptionHandler.handle("旧页签句柄已关闭?:", e);
                    if (windowHandles != null && !windowHandles.isEmpty()) {
                        //尝试切换到第一个页签,若成功则 复用浏览器;
                        browser.switchTo().window(windowHandles.iterator().next());
                        log.debug("已切换到第一个页签");
                        return;
                    }
                    //有异常:执行下面的代码
                } catch (WebDriverException e) {
                    AppExceptionHandler.handle("浏览器不可用?:", e);
                    //有异常:执行下面的代码
                }
            }
            //浏览器未打开,或不能复用浏览器:先尝试关闭,后打开
            log.debug("开始启动浏览器");
            closeBrowser();
            browserLifeState = BrowserLifeState.STARTING;//要重置状态

            //DriverService看似比较适合管理多个浏览器的生命周期,但没找到资料,智商有限,目前无法正常使用
            browser = browserType.starter.apply(headless);
            if (browserListener != null) browserListener.onOpened();
        } catch (Exception e) {
            boolean handled = false;
            if (browserListener != null) handled = browserListener.onOpenedFail(e);
            if (!handled) {
                ErrInfo errInfo = AppExceptionHandler.handle("打开" + browserType.desc + "浏览器错误:", e);
                throw new AppCommonException(errInfo.getErrMsg());
            }
        } finally {
            if (browser != null) {
                /*
                [浏览器首次启动,或旧页签句柄丢失重置后],第一次调用[selenium的api]时会等很久;
                这里调用一次,避免'在业务代码中等待太久'(影响业务代码的耗时计算)
                实现[本方法返回时,浏览器是马上可用的]
                 */
                String title = browser.getTitle();
                if (timerPicker.getTotalTime0() > 1000) {
                    //耗时太长时才有日志
                    log.info("已获取浏览器,耗时={},当前title=[{}]", timerPicker.getTotal0(), title);
                }
            }
        }
    }

    /**
     * 修改浏览器[无头]模式
     *
     * @apiNote 如果有改动, 则会关闭已有浏览器
     */
    public void setHeadless(boolean headless) {
        try {
            lock.lock();
            browserLifeState = BrowserLifeState.SETTING;
            if (this.headless == headless) return;
            this.headless = headless;
            if (isBrowserOpened()) {
                closeBrowser();
            }
        } finally {
            browserLifeState = BrowserLifeState.ACCESSIBLE;
            lock.unlock();
        }
    }

    /**
     * 获取一个已启动且马上可用的浏览器
     *
     * @apiNote 这可能是个耗时操作, {@link #testOpenBrowser()}
     */
    public WebDriver getBrowser() {
        try {
            lock.lock();
            browserLifeState = BrowserLifeState.STARTING;
            testOpenBrowser();
            return browser;
        } finally {
            browserLifeState = BrowserLifeState.ACCESSIBLE;
            lock.unlock();
        }
    }

    /**
     * 关闭浏览器(如果有打开的话)
     */
    public void closeBrowser() {
        try {
            lock.lock();
            browserLifeState = BrowserLifeState.CLOSING;
            if (browser != null) {
                log.debug("关闭浏览器");
                browser.quit();//这也是一个耗时操作:大概在1秒内
                browser = null;
                if (browserListener != null) browserListener.onClosed();
            }
        } finally {
            browserLifeState = BrowserLifeState.ACCESSIBLE;
            lock.unlock();
        }
    }

    /**
     * 关闭无页签的浏览器;
     * 处理谷歌浏览器,所有tab被关闭后浏览器会退出,但是driver进程却还在,但已经不能复用了!(目前没找到重连方法)
     *
     * @return 是否被关闭, 有页签时返回false
     * @apiNote 目前仅谷歌浏览器
     */
    public boolean closeBrowserIfNotTab(WebDriver browser) {
        try {
            lock.lock();
            browserLifeState = BrowserLifeState.SETTING;
            try {
                //没有页签时,浏览器被关闭,此方法会报错
                browser.getWindowHandles();
                return false;
            } catch (NoSuchSessionException e) {
                log.info("浏览器已无页签,将关闭浏览器");
                closeBrowser();
                return true;
            }
        } finally {
            browserLifeState = BrowserLifeState.ACCESSIBLE;
            lock.unlock();
        }
    }

    /**
     * @return 浏览器生命周期状态, {@link BrowserService} {@link BrowserLifeState}
     */
    public BrowserLifeState getBrowserLifeState() {
        return browserLifeState;
    }

    /**
     * @return 浏览器是否被打开, 本方法是线程不安全的, {@link #getBrowserLifeState()}
     */
    public boolean isBrowserOpened() {
        return browser != null;
    }

    /**
     * (简单的)使用当前页签打开网页
     * @apiNote 如果调用前当前页面是加载中的,则线程会一直阻塞!,直到已有页面加载完才加载当前url; {@link PageLoadStrategy}
     */
    public void load(String url) {
        getBrowser();//处理[浏览器启动,重置]的问题;
        browser.get(url);
    }

    /**
     * (简单的)加载url,并制定超时时长;
     * 实际情况,如果页面真的卡住了,那么实际超时的耗时很可能是ms的n倍(但至少是会停止的,而不会一直等下去)
     * @param ms 超时间隔ms
     * @apiNote 如果调用前当前页面是加载中的,则线程会一直阻塞!,直到已有页面加载完才加载当前url; {@link PageLoadStrategy}
     */
    public void load(String url, long ms) {
        if (ms <= 0) throw new IllegalArgumentException("入参必须大于0");
        getBrowser();
        long st = System.currentTimeMillis();
        try {
            browser.manage().timeouts().pageLoadTimeout(ms, TimeUnit.MILLISECONDS);
            browser.get(url);
        } catch (TimeoutException e) {
            long span = System.currentTimeMillis() - st;
            log.info("有超时异常,实际加载耗时={}, 期望耗时={}", span, ms);
        }
    }

    /**
     * 停止当前加载中的页面
     * @apiNote 目前此方法是无效的!!与 {@link PageLoadStrategy}相关, {@link BrowserType} 有说明
     */
    public void stopLoading() {
        if (browser == null) return;
        try {
            log.debug("st");
            Actions action = new Actions(browser);
            action.sendKeys(Keys.ESCAPE).perform();
            log.debug("ed");
        } catch (Exception e) {
            AppExceptionHandler.handle("停止错误:", e);
        }
    }

    public interface BrowserListener {
        void onOpened();

        /**
         * 浏览器打开失败
         *
         * @param e 失败的异常
         * @return false-业务代码未处理异常,本类将抛出此异常;true-业务代码已处理异常,本来后续无操作
         */
        boolean onOpenedFail(Exception e);

        void onClosed();
    }

}
